Исследование рынка заведений общественного питания Москвы¶
Содержание
- 1 Загрузка данных и подготовка к анализу
- 2 Предобработка данных
- 3 Исследовательский анализ данных
- 3.1 Распределение заведений по категориям
- 3.2 Распределение посадочных мест по категориям
- 3.3 Распределение сетевых и несетевых заведений
- 3.4 Топ-15 популярных сетей в Москве
- 3.5 Распределение заведений по районам Москвы
- 3.6 Распределение средних рейтингов по категориям заведений
- 3.7 Распределение средних рейтингов по районам Москвы
- 3.8 Распределение заведений по улицам Москвы
- 3.9 Стоимость среднего чека в зависимости от района
- 3.10 Зависимость режима работы от категорий и районов
- 3.11 Особенности заведений с плохими рейтингами
- 4 Целесообразность открытия кофейни
- 5 Выводы
Краткое описание проекта: инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Для этого необходимо подготовить исследование рынка Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.
Описание данных:
name— название заведения;address— адрес заведения;category— категория заведения, например «кафе», «пиццерия» или «кофейня»;hours— информация о днях и часах работы;lat— широта географической точки, в которой находится заведение;lng— долгота географической точки, в которой находится заведение;rating— рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);price— категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;avg_bill— строка, которая хранит среднюю стоимость заказа в виде диапазона, например:- «Средний счёт: 1000–1500 ₽»;
- «Цена чашки капучино: 130–220 ₽»;
- «Цена бокала пива: 400–600 ₽».
и так далее; middle_avg_bill— число с оценкой среднего чека, которое указано только для значений из столбцаavg_bill, начинающихся с подстроки «Средний счёт»:- Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
- Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
- Если значения нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.
middle_coffee_cup— число с оценкой одной чашки капучино, которое указано только для значений из столбцаavg_bill, начинающихся с подстроки «Цена одной чашки капучино»:- Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
- Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
- Если значения нет или оно не начинается с подстроки «Цена одной чашки капучино», то в столбец ничего не войдёт.
chain— число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):- 0 — заведение не является сетевым
- 1 — заведение является сетевым
district— административный район, в котором находится заведение, например Центральный административный округ;seats— количество посадочных мест.
План исследования:
- Выгрузка данных и первичное знакомство с данными.
- Изменение типов данных, обработка дубликатов и пропусков, добавление новых столбцов.
- Анализ распределения данных по категориям заведений.
- Выделение топ-15 популярных сетей общепита Москвы.
- Анализ распределения данных по районам заведений.
- Исследование зависимости режима работы от категории заведения.
- Выделение особенностей заведений с низким рейтингом.
- Исследование целесообразности открытия кофейни.
- Выводы.
Цель проекта: исследование рынка общепита Москвы и целесообразности открытия кофейни.
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import datetime as dt
import plotly.express as px
import folium
import re
import json
from scipy import stats as st
from statsmodels.stats import proportion as pr
from plotly import graph_objects as go
from scipy.stats import binom, norm
from math import sqrt
from IPython.display import display
from IPython.display import Image
from folium import Map, Marker, Choropleth
from folium.plugins import MarkerCluster
Загрузка данных и подготовка к анализу¶
catering = pd.read_csv('moscow_places.csv')
catering.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
print(f'Количество заведений: {catering["name"].nunique()}')
Количество заведений: 5614
Общее количество заведений в датасете составляет 5614. Из предварительного анализа видно, что в некоторых столбцах есть пропуски. Тип данных всех столбцов соответствует содержащейся в ней информации.
Предобработка данных¶
Обработка дубликатов¶
print(f'Количество дубликатов: {catering.duplicated().sum()}')
Количество дубликатов: 0
def name_standard(text): # функция для стандартизации названий
text = text.replace('ё', 'е')
text = text.replace('«', '')
text = text.replace('»', '')
text = text.lower()
dict =['кафе ', 'ресторан ', 'бар ', 'столовая ', 'кофейня ', 'пиццерия ', 'закусочная ']
for i in dict:
text = text.replace(i, '')
return text
catering['name_standard'] = catering['name'].apply(name_standard) #создаем новый столбец со стандартизированным названием
print(f'Количество неявных дубликатов: {catering[["name_standard","address", "seats"]].duplicated().sum()}')
Количество неявных дубликатов: 6
catering = catering.drop_duplicates(subset=['name_standard', 'address', 'seats']) # удаляем неявные дубликаты
Явные дубликаты в данных отсутствуют, неявные в количестве 6 штук были удалены.
Обработка пропусков¶
null_ratio = pd.DataFrame(round(catering.isna().mean()*100,1))
null_ratio.columns = ['null_ratio']
null_ratio
| null_ratio | |
|---|---|
| name | 0.0 |
| category | 0.0 |
| address | 0.0 |
| district | 0.0 |
| hours | 6.4 |
| lat | 0.0 |
| lng | 0.0 |
| rating | 0.0 |
| price | 60.5 |
| avg_bill | 54.6 |
| middle_avg_bill | 62.5 |
| middle_coffee_cup | 93.6 |
| chain | 0.0 |
| seats | 43.0 |
| name_standard | 0.0 |
catering['hours'] = catering['hours'].replace('Нет информации', 'NaN')
Пропуски в столбце hours заполнить невозможно. Они составляют 6,4% от общего количества записей, поэтому лучше их не удалять, так как возможна потеря важных данных.
Пропуски ценовой категории заведения в столбце price можно заполнить в соответствии с диапазонами значений среднего чека и ценой чашечки кофе в каждой из категории и районов:
catering['min'] = catering.groupby(['district', 'category'])['middle_avg_bill'].transform('min')
catering['max'] = catering.groupby(['district', 'category'])['middle_avg_bill'].transform('max')
catering['interval'] = (catering['max'] - catering['min']) / 4
def fill_na_price(row):
try:
if row['middle_avg_bill'] <= row['min'] + row['interval']:
return 'низкие'
elif row['middle_avg_bill'] <= row['min'] + 2 * row['interval']:
return 'средние'
elif row['middle_avg_bill'] <= row['min'] + 3 * row['interval']:
return 'выше среднего'
elif row['middle_avg_bill'] > row['min'] + 3 * row['interval']:
return 'высокие'
except:
pass
catering['price'] = catering['price'].fillna(catering[['min', 'interval',
'middle_avg_bill']].apply(fill_na_price, axis=1))
catering.drop(columns=['min', 'max', 'interval'], inplace=True)
catering['min'] = catering.groupby(['district', 'category'])['middle_coffee_cup'].transform('min')
catering['max'] = catering.groupby(['district', 'category'])['middle_coffee_cup'].transform('max')
catering['interval'] = (catering['max'] - catering['min']) / 4
def fill_na_price(row):
try:
if row['middle_coffee_cup'] <= row['min'] + row['interval']:
return 'низкие'
elif row['middle_coffee_cup'] <= row['min'] + 2 * row['interval']:
return 'средние'
elif row['middle_coffee_cup'] <= row['min'] + 3 * row['interval']:
return 'выше среднего'
elif row['middle_coffee_cup'] > row['min'] + 3 * row['interval']:
return 'высокие'
except:
pass
catering['price'] = catering['price'].fillna(catering[['min', 'interval',
'middle_coffee_cup']].apply(fill_na_price, axis=1))
catering.drop(columns=['min', 'max', 'interval'], inplace=True)
Посчитаем количество пропусков в price, которые можно заполнить на основании цены на бокал пива:
catering[~catering['avg_bill'].isna() & catering['middle_avg_bill'].isna()
& catering['middle_coffee_cup'].isna() & catering['price'].isna()]['name'].count()
16
Пропусков всего 16. Трудозатраты на их заполнение несоизмеримо больше, чем полученные результаты. Поэтому оставим их без изменений.
Оставшиеся пропуски, связанные с ценами, заполнить невозможно, поэтому оставим их без изменений.
Пропуски в количестве мест seats заполнить невозможно, поэтому оставим их без изменений.
Добавление новых столбцов¶
words = ['улица','ул','переулок','шоссе','проспект','площадь','проезд',
'село','аллея','бульвар','набережная','тупик','линия', 'МКАД']
str_pat = r".*,\s*\b([^,]*?(?:{})\b[^,]*)[,$]+".format("|".join(words))
catering['street'] = catering['address'].str.extract(str_pat)
catering['is_24/7'] = catering['hours'].str.contains('ежедневно, круглосуточно') # столбец с обозначением о работе 24/7
В ходе предобработки были обнаружены неявные дубликаты, которые были удалены. В данных сразу в нескольких столбцах есть пропуски, удалось заполнить лишь несколько пропусков в ценовой категории заведений (чуть больше 700), остальные столбцы остались без изменений. Также были добавлены столбцы с названием улицы и обозначением о работе 24/7.
Исследовательский анализ данных¶
Распределение заведений по категориям¶
df_name = catering.groupby('category', as_index=False).agg({'name':'count'}).sort_values(by='name')
df_name
| category | name | |
|---|---|---|
| 1 | булочная | 256 |
| 7 | столовая | 315 |
| 2 | быстрое питание | 603 |
| 5 | пиццерия | 633 |
| 0 | бар,паб | 764 |
| 4 | кофейня | 1413 |
| 6 | ресторан | 2039 |
| 3 | кафе | 2377 |
sns.set_style('darkgrid')
plt.subplots(figsize=(12,5))
sns.barplot(data = df_name, x='category', y='name', color='b')
plt.xlabel('Тип заведения')
plt.ylabel('Количество')
plt.title('Распределение заведений по категориям')
r = np.arange(len(df_name['category']))
for x,y in zip(r, df_name['name']):
label = "{:.0f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,1),
ha='center')
plt.show()
plt.subplots(figsize=(10,7))
plt.pie(df_name['name'], labels = df_name['category'], autopct = '%.2f%%')
plt.title('Распределение заведений по категориям')
plt.show()
Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%). Меньше всего булочных (3,05%).
Распределение посадочных мест по категориям¶
Количество посадочных мест будем анализировать с помощью среднего и медианы.
df_seats = catering.groupby('category',
as_index=False).agg({'seats':['mean', 'median']}).droplevel(1, axis=1)
df_seats.columns = ['category', 'mean', 'median']
df_seats['mean'] = df_seats['mean'].apply(lambda x: round(x, 1))
df_seats = df_seats.sort_values(by='median')
df_seats
| category | mean | median | |
|---|---|---|---|
| 1 | булочная | 89.4 | 50.0 |
| 5 | пиццерия | 94.5 | 55.0 |
| 3 | кафе | 97.4 | 60.0 |
| 2 | быстрое питание | 98.9 | 65.0 |
| 7 | столовая | 99.8 | 75.5 |
| 4 | кофейня | 111.2 | 80.0 |
| 0 | бар,паб | 124.5 | 82.0 |
| 6 | ресторан | 121.9 | 86.0 |
n = len(df_seats)
r = np.arange(n)
width = 0.25
fig, ax = plt.subplots(figsize=(15,8))
ax = plt.bar(r, df_seats['median'], width=width, label='median')
ax = plt.bar(r + width, df_seats['mean'], width=width, label='mean')
plt.xlabel('Категория')
plt.ylabel('Количество мест')
plt.title('Среднее и медиана посадочных мест по категориям')
plt.xticks(r + width/2, df_seats['category'])
plt.legend()
for x,y in zip(r, df_seats['median']):
label = "{:.1f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,5),
ha='center')
for x,y in zip(r + width, df_seats['mean']):
label = "{:.1f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,5),
ha='center')
plt.show()
plt.subplots(figsize=(12,7))
sns.boxplot(data=catering, x='category', y='seats')
plt.xlabel('Тип заведения')
plt.ylabel('Количество посадочных мест')
plt.title('Диаграмма размаха посадочных мест по категориям')
plt.show()
На графике видно, что в среднем меньше всего посадочных мест в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5).
На ящиках с усами заметно, что 3-й квартиль доходит вплоть до 150, а значения больше 300 являются выбросами.
Распределение сетевых и несетевых заведений¶
df_chain = catering.groupby('chain', as_index=False).agg({'name': 'count'})
df_chain
| chain | name | |
|---|---|---|
| 0 | 0 | 5197 |
| 1 | 1 | 3203 |
plt.subplots(figsize=(10,7))
plt.pie(df_chain['name'], labels = ['Несетевые', 'Сетевые'], autopct = '%.2f%%')
plt.title('Соотношение сетевых и несетевых заведений')
plt.show()
Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%).
df_category = catering.pivot_table(index='category',
values='name', columns='chain', aggfunc='count').reset_index()
df_category.columns = ['category', 'несетевые', 'сетевые']
df_category['chain_ratio'] = round(df_category['сетевые']/(df_category['сетевые']+df_category['несетевые'])*100,1)
df_category = df_category.sort_values(by='сетевые')
df_category
| category | несетевые | сетевые | chain_ratio | |
|---|---|---|---|---|
| 7 | столовая | 227 | 88 | 27.9 |
| 1 | булочная | 99 | 157 | 61.3 |
| 0 | бар,паб | 596 | 168 | 22.0 |
| 2 | быстрое питание | 371 | 232 | 38.5 |
| 5 | пиццерия | 303 | 330 | 52.1 |
| 4 | кофейня | 693 | 720 | 51.0 |
| 6 | ресторан | 1310 | 729 | 35.8 |
| 3 | кафе | 1598 | 779 | 32.8 |
plt.subplots(figsize=(20, 10))
sns.barplot(data = df_category, x='category', y='сетевые', ax=plt.subplot(1,2,1), color='g')
plt.xlabel('Тип заведения')
plt.ylabel('Количество сетевых заведений')
plt.title('Распределение сетевых заведений по категориям')
r = np.arange(len(df_category['category']))
for x,y in zip(r, df_category['сетевые']):
label = "{:.0f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,5),
ha='center')
sns.barplot(data = df_category, x='category', y='chain_ratio', ax=plt.subplot(1,2,2), color='y')
plt.xlabel('Тип заведения')
plt.ylabel('Доля сетевых заведений')
plt.title('Доли сетевых заведений по категориям')
for x,y in zip(r, df_category['chain_ratio']):
label = "{:.1f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,5),
ha='center')
plt.tight_layout()
plt.show()
plt.subplots(figsize=(10,7))
plt.pie(df_category['сетевые'], labels = df_category['category'], autopct = '%.2f%%')
plt.title('Распределение сетевых заведений по категориям')
plt.show()
Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%).
При этом по доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%).
Кофейня занимает 3-е место по количеству сетевых заведений как в абсолютном, так и в относительном выражении. Поэтому при открытии сетевого заведения стоит обратить внимание именно на данную категорию.
Топ-15 популярных сетей в Москве¶
df_top = catering[catering['chain']==1].groupby(
'name', as_index=False).agg({'category':'first','chain':'count'}).sort_values(by='chain', ascending=False).head(15)
df_top
| name | category | chain | |
|---|---|---|---|
| 746 | Шоколадница | кофейня | 120 |
| 344 | Домино'с Пицца | пиццерия | 76 |
| 340 | Додо Пицца | пиццерия | 74 |
| 148 | One Price Coffee | кофейня | 71 |
| 759 | Яндекс Лавка | ресторан | 69 |
| 59 | Cofix | кофейня | 65 |
| 170 | Prime | ресторан | 50 |
| 679 | Хинкальная | быстрое питание | 44 |
| 378 | КОФЕПОРТ | кофейня | 42 |
| 431 | Кулинарная лавка братьев Караваевых | кафе | 39 |
| 643 | Теремок | ресторан | 38 |
| 699 | Чайхана | кафе | 37 |
| 40 | CofeFest | кофейня | 32 |
| 273 | Буханка | булочная | 32 |
| 491 | Му-Му | кафе | 27 |
fig = px.bar(df_top.sort_values(by='chain'),
x='chain',
y='name',
text='category'
)
fig.update_layout(title='ТОП-15 сетей по количеству заведений',
xaxis_title='Количество заведений',
yaxis_title='Название сети')
fig.show()
df_cnt = df_top.groupby('category', as_index=False).agg({'name':'count'}).sort_values('name', ascending=False)
plt.subplots(figsize=(12,5))
sns.barplot(data = df_cnt, x='category', y='name', color='r')
plt.xlabel('Тип заведения')
plt.ylabel('Количество')
plt.title('Распределение Топ-15 сетей по категориям')
plt.show()
Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети).
Таким образом, в Топ-15 ярче всего представлены кофейни, а на 1-ом месте по количеству заведений сеть кофеен "Шоколадница". Это говорит о целесообразности открытия сети именно данной категории.
Распределение заведений по районам Москвы¶
df_district = catering.pivot_table(index='district', values='name', aggfunc='count', columns='category').reset_index()
def acronym(some_words): #функция для создания акронима
return ''.join(re.findall(r'\b\w', some_words)).upper()
for row in range(len(df_district['district'])): #заменяем названия округов на акронимы
df_district['district'] = df_district.replace(df_district.loc[row,'district'], acronym(df_district.loc[row,'district']))
df_district = df_district.set_index('district')
df_district['total'] = df_district.sum(axis=1)
df_district = df_district.sort_values(by='total')
df_district
| category | бар,паб | булочная | быстрое питание | кафе | кофейня | пиццерия | ресторан | столовая | total |
|---|---|---|---|---|---|---|---|---|---|
| district | |||||||||
| СЗАО | 23 | 12 | 30 | 115 | 62 | 40 | 109 | 18 | 409 |
| ЮЗАО | 38 | 27 | 61 | 238 | 96 | 64 | 168 | 17 | 709 |
| ЮВАО | 38 | 13 | 67 | 282 | 89 | 55 | 144 | 25 | 713 |
| ВАО | 53 | 25 | 71 | 272 | 105 | 72 | 160 | 40 | 798 |
| ЗАО | 50 | 37 | 62 | 238 | 150 | 71 | 218 | 24 | 850 |
| СВАО | 62 | 28 | 82 | 269 | 159 | 68 | 181 | 40 | 889 |
| ЮАО | 68 | 25 | 85 | 264 | 131 | 73 | 202 | 44 | 892 |
| САО | 68 | 39 | 58 | 235 | 193 | 77 | 188 | 41 | 899 |
| ЦАО | 364 | 50 | 87 | 464 | 428 | 113 | 669 | 66 | 2241 |
plt.subplots(figsize=(12,7))
sns.heatmap(df_district.loc[:, df_district.columns != 'total'], annot=True, linewidths=1, fmt='d')
plt.xlabel('Категория')
plt.ylabel('Район')
plt.yticks(rotation = 360)
plt.title('Распределение категорий заведений по районам')
plt.show()
fig, ax = plt.subplots(figsize=(15,10))
ax1 = ax.bar(df_district.index, df_district['кафе'], label='кафе')
ax2 = ax.bar(df_district.index, df_district['ресторан'], bottom=df_district['кафе'], label='ресторан')
ax3 = ax.bar(df_district.index, df_district['кофейня'], bottom=df_district['кафе']+df_district['ресторан'], label='кофейня')
ax4 = ax.bar(df_district.index, df_district['бар,паб'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня'], label='бар,паб')
ax5 = ax.bar(df_district.index, df_district['пиццерия'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб'], label='пиццерия')
ax6 = ax.bar(df_district.index, df_district['быстрое питание'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия'], label='быстрое питание')
ax7 = ax.bar(df_district.index, df_district['столовая'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия']+df_district['быстрое питание'], label='столовая')
ax8 = ax.bar(df_district.index, df_district['булочная'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия']+df_district['быстрое питание']+df_district['столовая'], label='булочная')
ax.set_xlabel('Район')
ax.set_ylabel('Количество заведений')
ax.set_title('Распределение категорий заведений по районам')
plt.legend()
plt.show()
На тепловой карте и столбчатой диаграмме выделяется ЦАО, в нем заведений 2241, что более чем в 2 раза больше, чем в любом другом округе. Отдельно стоит выделить рестораны (669), кафе (464), кофейни (428), бары (364), цвет которых на тепловой карте является более светлым, чем все остальные ячейки.
Меньше всего заведений в СЗАО (409). В остальных округах заведений примерно поровну.
На столбчатой диаграмме хорошо видно, что рестораны, кафе и кофейни являются топ-3 категориями во всех округах. Это также может говорить о целесообразности открытия кофейни по сравнению с большинством других категорий, так как кофейни являются довольно популярными.
Распределение средних рейтингов по категориям заведений¶
df_rating = catering.groupby('category')['rating'].agg(['mean', 'median']).reset_index()
df_rating['mean'] = round(df_rating['mean'], 2)
df_rating=df_rating.sort_values(by='mean')
df_rating
| category | mean | median | |
|---|---|---|---|
| 2 | быстрое питание | 4.05 | 4.2 |
| 3 | кафе | 4.12 | 4.2 |
| 7 | столовая | 4.21 | 4.3 |
| 1 | булочная | 4.27 | 4.3 |
| 4 | кофейня | 4.28 | 4.3 |
| 6 | ресторан | 4.29 | 4.3 |
| 5 | пиццерия | 4.30 | 4.3 |
| 0 | бар,паб | 4.39 | 4.4 |
За основу для анализа возьмем среднюю, а не медиану, так как рейтинги весьма близки друг к другу и средняя лучше отражает отличие между категориями. Кроме того, средняя и медиана по всем категориям несильно отличаются друг от друга.
plt.subplots(figsize=(12,5))
sns.barplot(data = df_rating, x='category', y='mean', color='k')
plt.ylim(4, 4.5)
plt.xlabel('Тип заведения')
plt.ylabel('Средний рейтинг')
plt.title('Средний рейтинг заведений по категориям')
r = np.arange(len(df_rating['category']))
for x,y in zip(r, df_rating['mean']):
label = "{:.2f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,5),
ha='center')
plt.show()
plt.subplots(figsize=(12,7))
sns.boxplot(data=catering, x='category', y='rating')
plt.xlabel('Тип заведения')
plt.ylabel('Рейтинг')
plt.title('Диаграмма размаха рейтинга по категориям')
plt.show()
На столбчатой диаграмме видно, что средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4, то есть отличаются несильно друг от друга. Однако четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
Кофейни находятся примерно посередине (4,28).
В кафе и барах нормальными считаются рейтинги вплоть до 5. Самый низкий 25-процентный квартиль у заведений быстрого питания.
Распределение средних рейтингов по районам Москвы¶
rating_df = catering.groupby('district')['rating'].agg(['median', 'mean']).reset_index()
rating_df['mean'] = round(rating_df['mean'], 2)
rating_df
| district | median | mean | |
|---|---|---|---|
| 0 | Восточный административный округ | 4.3 | 4.17 |
| 1 | Западный административный округ | 4.3 | 4.18 |
| 2 | Северный административный округ | 4.3 | 4.24 |
| 3 | Северо-Восточный административный округ | 4.2 | 4.15 |
| 4 | Северо-Западный административный округ | 4.3 | 4.21 |
| 5 | Центральный административный округ | 4.4 | 4.38 |
| 6 | Юго-Восточный административный округ | 4.2 | 4.10 |
| 7 | Юго-Западный административный округ | 4.3 | 4.17 |
| 8 | Южный административный округ | 4.3 | 4.18 |
with open('admin_level_geomap.geojson', 'r', encoding='utf-8') as f: # открываем файл geojson
state_geo = json.load(f)
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=rating_df,
columns=['district', 'mean'], # за основу возьмем средний рейтинг, он лучше медианного отображает разницу между районами
key_on='feature.name',
fill_color='YlOrRd',
fill_opacity=0.8,
legend_name='Средний рейтинг заведений по районам',
).add_to(m)
# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(m)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
catering.apply(create_clusters, axis=1)
# выводим карту
m
На фоновой картограмме видно, что самый высокий рейтинг (4,38) и наибольшее количество заведений (2241) находится в ЦАО, что соответствует выводам из прошлых графиков. Также на карте заметно постепенное падение числа заведений по мере отдаления от центра города. Самый низкий рейтинг у ЮВАО (4,1).
ЦАО выделяется по многим критериям: в нем больше всего количество заведений, самый высокий средний рейтинг, средний чек.
Высокое количество заведений связано с самым высоким спросом на услуги общепита по сравнению с другими районами.
Самый высокий рейтинг говорит о высоком качестве предоставляемых услуг.
Высокий средний чек связан с более высоким уровнем цен на фоне более высокого спроса и качества услуг.
Другим фактором является то, что жители ЦАО в среднем богаче, чем жители остальных районов.
В ЦАО также расположены офисы многих престижных компаний, которые платят своим работникам хорошие зарплаты, которые потом в том числе тратятся в заведениях общепита.
ЦАО является центральным районом города, соответственно сюда приезжает большое число туристов, которые также тратят деньги в заведениях общепита. Из-за центрального расположения здесь также высокая стоимость аренды.
Распределение заведений по улицам Москвы¶
df_street = catering.pivot_table(index='street', values='name', aggfunc='count', columns='category').fillna(0).astype('int')
df_street['total'] = df_street.sum(axis=1)
df_top_street = df_street.sort_values(by='total', ascending=False).head(15).sort_values(by='total')
df_top_street
| category | бар,паб | булочная | быстрое питание | кафе | кофейня | пиццерия | ресторан | столовая | total |
|---|---|---|---|---|---|---|---|---|---|
| street | |||||||||
| Пятницкая улица | 9 | 3 | 2 | 7 | 6 | 3 | 18 | 0 | 48 |
| улица Миклухо-Маклая | 3 | 0 | 4 | 21 | 4 | 2 | 15 | 0 | 49 |
| Кутузовский проспект | 2 | 1 | 2 | 14 | 13 | 3 | 16 | 3 | 54 |
| улица Вавилова | 2 | 2 | 11 | 15 | 10 | 3 | 12 | 0 | 55 |
| Люблинская улица | 5 | 0 | 5 | 26 | 11 | 1 | 10 | 2 | 60 |
| МКАД | 1 | 0 | 9 | 45 | 4 | 0 | 5 | 1 | 65 |
| Ленинградское шоссе | 5 | 2 | 5 | 13 | 13 | 3 | 25 | 3 | 69 |
| Варшавское шоссе | 6 | 0 | 7 | 17 | 14 | 4 | 20 | 7 | 75 |
| Каширское шоссе | 2 | 0 | 10 | 20 | 16 | 5 | 19 | 5 | 77 |
| Дмитровское шоссе | 6 | 2 | 10 | 23 | 11 | 8 | 24 | 4 | 88 |
| Ленинградский проспект | 15 | 4 | 2 | 12 | 25 | 9 | 25 | 3 | 95 |
| Ленинский проспект | 10 | 3 | 2 | 26 | 23 | 5 | 33 | 5 | 107 |
| проспект Вернадского | 7 | 1 | 12 | 25 | 16 | 12 | 33 | 2 | 108 |
| Профсоюзная улица | 6 | 4 | 15 | 35 | 18 | 15 | 26 | 3 | 122 |
| проспект Мира | 11 | 4 | 21 | 53 | 36 | 11 | 45 | 2 | 183 |
plt.subplots(figsize=(12,7))
sns.heatmap(df_top_street.loc[:, df_top_street.columns != 'total'], annot=True, linewidth=1, fmt='d')
plt.xlabel('Категория')
plt.ylabel('Улица')
plt.yticks(rotation = 360)
plt.title('Распределение категорий заведений по улицам')
plt.show()
fig, ax = plt.subplots(figsize=(15,10))
ax.barh(df_top_street.index, df_top_street['кафе'], label='кафе')
ax.barh(df_top_street.index, df_top_street['ресторан'], left=df_top_street['кафе'], label='ресторан')
ax.barh(df_top_street.index, df_top_street['кофейня'], left=df_top_street['кафе']+df_top_street['ресторан'], label='кофейня')
ax.barh(df_top_street.index, df_top_street['бар,паб'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня'], label='бар,паб')
ax.barh(df_top_street.index, df_top_street['пиццерия'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб'], label='пиццерия')
ax.barh(df_top_street.index, df_top_street['быстрое питание'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия'], label='быстрое питание')
ax.barh(df_top_street.index, df_top_street['столовая'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия']+df_top_street['быстрое питание'], label='столовая')
ax.barh(df_top_street.index, df_top_street['булочная'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия']+df_top_street['быстрое питание']+df_top_street['столовая'], label='булочная')
ax.set_xlabel('Количество заведений')
ax.set_ylabel('Улица')
ax.set_title('Распределение категорий заведений по улицам')
plt.legend()
plt.show()
Улицей с наибольшим количеством заведений является Проспект Мира (183). На тепловой карте самой светлой ячейкой является кафе на данной улице (53).
На столбчатой диаграмме видно, что в большинстве улиц самыми популярными являются кафе, рестораны и кофейни.
df_1 = df_street[df_street['total']==1]
df_total = pd.DataFrame(df_1.sum(axis=0)).sort_values(by=0).reset_index()
df_total.columns = ['category', 'count']
df_total = df_total[df_total.index!=8]
df_total
| category | count | |
|---|---|---|
| 0 | булочная | 9 |
| 1 | пиццерия | 14 |
| 2 | быстрое питание | 17 |
| 3 | столовая | 36 |
| 4 | бар,паб | 40 |
| 5 | кофейня | 73 |
| 6 | ресторан | 88 |
| 7 | кафе | 137 |
plt.subplots(figsize=(12,5))
sns.barplot(data = df_total, x='category', y='count', color='b')
plt.xlabel('Тип заведения')
plt.ylabel('Количество заведений')
plt.title('Распределение по категориям заведений на улицах с одним заведением')
r = np.arange(len(df_total['category']))
for x,y in zip(r, df_total['count']):
label = "{:.0f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,3),
ha='center')
plt.show()
На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73).
Стоимость среднего чека в зависимости от района¶
df_bill = catering.groupby('district').agg({'middle_avg_bill':'median'}).reset_index()
df_bill
| district | middle_avg_bill | |
|---|---|---|
| 0 | Восточный административный округ | 575.0 |
| 1 | Западный административный округ | 1000.0 |
| 2 | Северный административный округ | 650.0 |
| 3 | Северо-Восточный административный округ | 500.0 |
| 4 | Северо-Западный административный округ | 700.0 |
| 5 | Центральный административный округ | 1000.0 |
| 6 | Юго-Восточный административный округ | 450.0 |
| 7 | Юго-Западный административный округ | 600.0 |
| 8 | Южный административный округ | 500.0 |
# создаём карту Москвы
k = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=df_bill,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='YlGnBu',
fill_opacity=0.8,
legend_name='Средний чек заведений по районам',
).add_to(k)
# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(k)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['middle_avg_bill']}",
).add_to(marker_cluster)
catering.apply(create_clusters, axis=1)
# выводим карту
k
На хороплете видно, что самый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается.
Зависимость режима работы от категорий и районов¶
catering['hours'].nunique()
1306
catering.groupby('hours').agg({'name':'count'}).sort_values(by='name', ascending=False).head(50)['name'].sum()
5233
Все 1306 групп по режиму работы рассмотреть проблематично, да и не имеет смысла, так как многочисленные группы не удастся корректно отобразить и сравнить с малочисленными группами на одном графике.
Более того, многочисленные группы представляют собой более репрезентативную выборку, так как чем более малочисленной является группа, тем реже она встречается и тем слабее влияет на общее распределение данных.
Тенденции, которые можно заметить по многочисленным группам, чаще всего имеют место и в малочисленных группах. В небольшом количестве случаев, когда это не так, малочисленные группы все равно не смогут сильно повлиять на результат в силу маленького количества наблюдений в них.
На 20 наиболее многочисленных режимов работы приходится около половины (4204) совокупного количества наблюдений.
В целях анализа возьмем 50 наиболее распространенных режимов работы, этого вполне достаточно для выявления зависимостей между режимом работы и районом, категорией заведения.
df_work = catering.pivot_table(index='hours', values='name', aggfunc='count', columns='category').fillna(0)
df_work['total'] = df_work.sum(axis=1)
df_work = df_work.sort_values(by='total', ascending=False).head(50)
plt.subplots(figsize=(15,12))
sns.heatmap(df_work.loc[:, df_work.columns != 'total'], annot=True, fmt='.0f', linewidth=1)
plt.xlabel('Категория')
plt.ylabel('Режим работы')
plt.title('Распределение категорий заведений по режимам работы')
plt.show()
Наиболее часто встречаются работающие круглосуточно кафе (267), далее идут рестораны, работающие с 10 до 22 (192) или с 11 до 23 (175).
Бары и пабы в основном работают ежедневно в 12-часовом режиме с позднего утра (с 11 или 12) до позднего вечера (до 23 или 00).
Булочные в основном работают ежедневно с раннего утра (с 8 или с 9) до вечера (до 22 или 21). Также встречается круглосуточный режим работы.
Заведения быстрого питания работают ежедневно, многие круглосуточно или с 10 до 22.
Кафе работают в самые разные часы, однако чаще всего встречается круглосуточный режим работы или с 10 до 22.
Кофейни также работают в самое разное время, однако большая часть с утра до вечера (например с 10 до 22). Круглосуточные тоже встречаются.
Пиццерии работают в основном с 10 до 22 (или 23).
Рестораны работают в самое разное время, чаще с 10 до 22 или с 11 до 23. Встречается и круглосуточный режим работы.
Столовые работают только по будним дням с 9 до 18 (или до 17).
df_dst = catering.pivot_table(index='hours', values='name', aggfunc='count', columns='district').fillna(0)
counter=[]
for i in df_dst.columns.to_list(): # приводим названия районов к акронимам
counter.append(acronym(i))
df_dst.columns = counter
df_dst['total'] = df_dst.sum(axis=1)
df_dst = df_dst.sort_values(by='total', ascending=False).head(50)
plt.subplots(figsize=(15,12))
sns.heatmap(df_dst.loc[:, df_dst.columns != 'total'], annot=True, fmt='.0f', linewidth=1)
plt.xlabel('Район')
plt.ylabel('Режим работы')
plt.title('Распределение районов по режимам работы заведений')
plt.show()
Тепловая карта распределения по районам немного более контрастная, чем карта по категориям. Это объясняется в том числе разницей в диапазонах разброса данных (в карте по районам диапазон значений примерно в 2 раза меньше, чем в карте по категориям).
На тепловой карте видно, что в ЦАО встречаются самые разные режимы работы, однако чаще всего круглосуточный (130 заведений), за ним идет с 12 до 0 (120). Такое разнообразие объясняется тем, что в ЦАО находится намного больше заведений, чем в любом другом районе (более чем в 2 раза). Поэтому для более тщательного сравнения других районов между собой (но такой цели не стоит) имеет смысл исключить ЦАО из анализа.
По аналогии "темнота" в СЗАО также объясняется сильно меньшим количеством заведений в данном районе по сравнению с другими.
В целом на данной карте очень отчетливо видно, что в Москве большинство заведений работает в круглосуточном режиме, либо с 10 до 22 (в том числе и в СЗАО).
Особенности заведений с плохими рейтингами¶
plt.subplots(figsize=(10,5))
sns.histplot(data=catering, x="rating", bins=10)
plt.xlabel('Рейтинг')
plt.ylabel('Количество')
plt.title('Распределение заведений по рейтингу')
plt.show()
На гистограмме видно, что у абсолютного большинства заведений рейтинг от 4 до 4,5, а по мере снижения рейтинга количество заведений падает. В качестве заведений с низким рейтингом целесообразно рассмотреть заведения с рейтингом меньше 3 баллов (3 балла делят выборку пополам).
g = sns.pairplot(catering, y_vars = ['rating'], x_vars=['middle_avg_bill', 'middle_coffee_cup', 'seats'])
g.fig.set_size_inches(15,5)
g.axes[0,0].set_ylabel('Рейтинг')
g.axes[0,0].set_xlabel('Средний чек')
g.axes[0,1].set_xlabel('Средняя цена чашки кофе')
g.axes[0,2].set_xlabel('Количество посадочных мест')
g.axes[0,1].set_title('Диаграммы рассеяния рейтинга заведений')
plt.show()
print(f'Корреляция между рейтингом и средним чеком: {round(catering["rating"].corr(catering["middle_avg_bill"]),4)}')
print(f'Корреляция между рейтингом и средней ценой чашки кофе: {round(catering["rating"].corr(catering["middle_coffee_cup"]),4)}')
print(f'Корреляция между рейтингом и количеством посдочных мест: {round(catering["rating"].corr(catering["seats"]),4)}')
Корреляция между рейтингом и средним чеком: 0.1832 Корреляция между рейтингом и средней ценой чашки кофе: 0.1004 Корреляция между рейтингом и количеством посдочных мест: 0.0211
На диаграммах рассеяния видно, что по заведениям с низким рейтингом наблюдений меньше, чем с высоким.
В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей (есть одна аномалия с чеком около 5000 рублей).
Средняя цена чашки кофе не превышает 100 рублей.
Количество посадочных мест не превышает 400.
Корреляция между показателями положитальная слабая или практически отсутствует. Отметить только можно слабую прямую зависимость между рейтингом и средним чеком.
df_low = catering[catering['rating']<3].groupby('category').agg({'name':'count'}).reset_index().sort_values(by='name')
df_low
| category | name | |
|---|---|---|
| 1 | булочная | 2 |
| 7 | столовая | 6 |
| 0 | бар,паб | 8 |
| 5 | пиццерия | 8 |
| 4 | кофейня | 16 |
| 2 | быстрое питание | 31 |
| 6 | ресторан | 32 |
| 3 | кафе | 107 |
plt.subplots(figsize=(12,5))
sns.barplot(data = df_low, x='category', y='name', color='r')
plt.xlabel('Тип заведения')
plt.ylabel('Количество заведений')
plt.title('Распределение заведений с низким рейтингом по категориям')
r = np.arange(len(df_low['category']))
for x,y in zip(r, df_low['name']):
label = "{:.0f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,3),
ha='center')
plt.show()
plt.subplots(figsize=(10,7))
plt.pie(df_low['name'], labels = df_low['category'], autopct = '%.2f%%')
plt.title('Распределение заведений с низким рейтингом по категориям')
plt.show()
На столбчатой диаграмме видно, что абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).
По итогам анализа можно подвести следующие итоги:
- Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%);
- Посадочных мест меньше всего в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5);
- Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%);
- Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%);
- По доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%);
- Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети);
- Больше всего заведений в ЦАО (2241), меньше всего в СЗАО (409). В остальных округах заведений примерно поровну;
- Средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4. Четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
- На карте заметно постепенное падение числа заведений по мере отдаления от центра города;
- По округам самый высокий рейтинг у ЦАО (4,38), а самый низкий - у ЮВАО (4,1);
- Улицей с наибольшим количеством заведений является Проспект Мира (183, из них 53 - кафе);
- На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73);
- Cамый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается;
- Большинство заведений работает ежедневно с 10 до 22, также часто встречается режим работы 24/7. Также режим работы зависит от типа заведения. Например столовые работают в основном с 9 до 18 (или 17);
- В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей. Средняя цена чашки кофе не превышает 100 рублей. Количество посадочных мест не превышает 400;
- Абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).
Целесообразность открытия кофейни¶
Особенности расположения кофеен¶
df_cof = catering[catering["category"]=="кофейня"]
print(f'Количество кофеен: {df_cof["name"].count()}')
Количество кофеен: 1413
df_cofdst = df_cof.groupby('district', as_index=False).agg({'name':'count'}).sort_values(by='name')
for row in range(len(df_cofdst['district'])): #заменяем названия округов на акронимы
df_cofdst['district'] = df_cofdst.replace(df_cofdst.loc[row,'district'], acronym(df_cofdst.loc[row,'district']))
df_cofdst = df_cofdst.merge(df_district['total'], on='district')
df_cofdst['ratio'] = round(df_cofdst['name']/df_cofdst['total']*100,2)
df_cofdst
| district | name | total | ratio | |
|---|---|---|---|---|
| 0 | СЗАО | 62 | 409 | 15.16 |
| 1 | ЮВАО | 89 | 713 | 12.48 |
| 2 | ЮЗАО | 96 | 709 | 13.54 |
| 3 | ВАО | 105 | 798 | 13.16 |
| 4 | ЮАО | 131 | 892 | 14.69 |
| 5 | ЗАО | 150 | 850 | 17.65 |
| 6 | СВАО | 159 | 889 | 17.89 |
| 7 | САО | 193 | 899 | 21.47 |
| 8 | ЦАО | 428 | 2241 | 19.10 |
plt.subplots(figsize=(20,10))
sns.barplot(data = df_cofdst, x='district', y='name', ax=plt.subplot(1,2,1), color = 'y')
plt.xlabel('Район')
plt.ylabel('Количество кофеен')
plt.title('Распределение кофеен по районам')
r = np.arange(len(df_cofdst['district']))
for x,y in zip(r, df_cofdst['name']):
label = "{:.0f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,3),
ha='center')
sns.barplot(data = df_cofdst, x='district', y='ratio', ax=plt.subplot(1,2,2), color = 'g')
plt.xlabel('Район')
plt.ylabel('Доля кофеен, %')
plt.title('Доли кофеен по районам')
for x,y in zip(r, df_cofdst['ratio']):
label = "{:.2f}".format(y)
plt.annotate(label,
(x, y),
textcoords="offset points",
xytext=(0,3),
ha='center')
plt.tight_layout
plt.show()
plt.subplots(figsize=(10,7))
plt.pie(df_cofdst['name'], labels = df_cofdst['district'], autopct = '%.2f%%')
plt.title('Распределение кофеен по районам')
plt.show()
Большая часть кофеен находится в ЦАО (доля 30,29%), далее идут САО (13,66%) и СВАО (11,25%).
По доле кофеен от всех заведений ЦАО (19,10%) занимает 2-е место после САО (21,47%). На 3-ем и 4-ом местах СВАО (17,89%) и ЗАО (17,65%).
Режим работы кофеен¶
print(f'Количество круглосуточных кофеен: {df_cof[df_cof["is_24/7"]==True]["name"].count()}')
print(f'Доля круглосуточных кофеен: {round(df_cof["is_24/7"].mean()*100,2)}%')
Количество круглосуточных кофеен: 59 Доля круглосуточных кофеен: 4.22%
df_24 = df_cof.groupby('is_24/7').agg({'name':'count'})
plt.subplots(figsize=(10,7))
plt.pie(df_24['name'], labels = ['Некруглосуточный', 'Круглосуточный'], autopct = '%.2f%%')
plt.title('Режим работы кофеен')
plt.show()
4,22% из всех кофеен (по которым есть данные) работают круглосуточно.
df_chain = df_cof.groupby('chain').agg({'name':'count'})
plt.subplots(figsize=(10,7))
plt.pie(df_chain['name'], labels = ['Несетевые', 'Сетевые'], autopct = '%.2f%%')
plt.title('Сетевые и несетевые кофейни')
plt.show()
Сетевых и несетевых кофеен примерно поровну, 49,04% и 50,96% соответственно.
Распределение рейтингов кофеен по районам¶
cof_rat = df_cof.groupby('district').agg({'rating': ['mean', 'median']}).reset_index().droplevel(1, axis=1)
cof_rat.columns=['district', 'mean', 'median']
cof_rat['mean'] = round(cof_rat['mean'], 2)
cof_rat
| district | mean | median | |
|---|---|---|---|
| 0 | Восточный административный округ | 4.28 | 4.3 |
| 1 | Западный административный округ | 4.20 | 4.2 |
| 2 | Северный административный округ | 4.29 | 4.3 |
| 3 | Северо-Восточный административный округ | 4.22 | 4.3 |
| 4 | Северо-Западный административный округ | 4.33 | 4.3 |
| 5 | Центральный административный округ | 4.34 | 4.3 |
| 6 | Юго-Восточный административный округ | 4.23 | 4.3 |
| 7 | Юго-Западный административный округ | 4.28 | 4.3 |
| 8 | Южный административный округ | 4.23 | 4.3 |
Медианный рейтинг почти во всех районах одинаковый, поэтому будем использовать среднюю, чтобы отразить отличие между районами.
# создаём карту Москвы
a = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=cof_rat,
columns=['district', 'mean'],
key_on='feature.name',
fill_color='Greens',
fill_opacity=0.8,
legend_name='Средний рейтинг заведений по районам',
).add_to(a)
# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(a)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
df_cof.apply(create_clusters, axis=1)
# выводим карту
a
Максимально высокий средний рейтинг в ЦАО (4,34) и в СЗАО (4,33). В целом рейтинги по районам мало отличаются друг от друга, они находятся в диапазоне от 4,2 до 4,34.
Распределение средней стоимости чашки капучино по районам¶
cof_cup = df_cof.groupby('district').agg({'middle_coffee_cup': ['mean', 'median']}).reset_index().droplevel(1, axis=1)
cof_cup.columns=['district', 'mean', 'median']
cof_cup['mean'] = round(cof_cup['mean'], 2)
cof_cup
| district | mean | median | |
|---|---|---|---|
| 0 | Восточный административный округ | 174.02 | 135.0 |
| 1 | Западный административный округ | 189.94 | 189.0 |
| 2 | Северный административный округ | 165.79 | 159.0 |
| 3 | Северо-Восточный административный округ | 165.33 | 162.5 |
| 4 | Северо-Западный административный округ | 165.52 | 165.0 |
| 5 | Центральный административный округ | 187.52 | 190.0 |
| 6 | Юго-Восточный административный округ | 151.09 | 147.5 |
| 7 | Юго-Западный административный округ | 184.18 | 198.0 |
| 8 | Южный административный округ | 158.49 | 150.0 |
# создаём карту Москвы
b = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=cof_cup,
columns=['district', 'median'],
key_on='feature.name',
fill_color='RdPu',
fill_opacity=0.8,
legend_name='Медианная стоимость чашки капучино заведений по районам',
).add_to(b)
# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(b)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['middle_coffee_cup']}",
).add_to(marker_cluster)
df_cof.apply(create_clusters, axis=1)
# выводим карту
b
Максимальная медианная стоимость чашки капучино в ЦАО (190), ЗАО (189), ЮЗАО (198). В целом она находится в диапазоне от 135 до 198.
Рекомендации по открытию кофейни¶
- Наиболее популярным районом является ЦАО (428 кофейни). Большое количество заведений означает серьезную конкуренцию, но открытие кофейни в данном районе может окупиться за счет большого спроса. Наименьшее количество кофеен в СЗАО (62), возможно здесь удастся заполнить пустующую нишу. Однако можно открыть кофейню и в любом другом районе - главное выбрать удачное место с высокой проходимостью;
- Абсолютное большинство кофеен (более 95%) работает не в круглосуточном режиме, что подтверждает нерентабельность круглосуточного режима работы кофейни. Что касается сетей, то результаты анализа говорят о равном шансе на успех при открытии сетевого (51%) или несетевого (49%) заведения;
- Рейтинги заведений по районам мало отличаются друг от друга, однако в ЦАО (4,34) и СЗАО (4,33) они наибольшие;
- В ЦАО медианная стоимость чашки капучино равна 190, что является одним из самых больших показателей. При открытии кофейни в любом из районов следует ориентироваться именно на медианную стоимость чашки в этом районе.
Таким образом, можно рассмотреть бизнес-проект с открытием кофейни в ЦАО, работающую ежедневно с 10 до 22. Стоимость чашки капучино при этом должна быть равна примерно 190. Количество посадочных мест зависит от планируемой проходимости и спроса.
Отдельно стоит отметить недостаток данных в датасете. Для корректного анализа инвестиционного проекта необходимы некоторые другие показатели, которых в данных нет. Например, средняя проходимость, объем затрат на аренду, затрат на заработную плату и т.д. Без них невозможно корректно рассчитать денежные потоки и срок окупаемости инвестиций.
Выводы¶
В ходе предобработки данных были выявлены следующие проблемы:
- Неявные дубликаты (6), которые были удалены;
- Пропуски, по ценовой категории удалось восстановить около 700 пропусков, остальные пропуски остались без изменения;
- Тип данных количества посадочных мест был изменен на целочисленный.
По итогам анализа можно подвести следующие итоги:
- Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%);
- Посадочных мест меньше всего в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5);
- Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%);
- Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%);
- По доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%);
- Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети);
- Больше всего заведений в ЦАО (2241), меньше всего в СЗАО (409). В остальных округах заведений примерно поровну;
- Средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4. Четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
- На карте заметно постепенное падение числа заведений по мере отдаления от центра города;
- По округам самый высокий рейтинг у ЦАО (4,38), а самый низкий - у ЮВАО (4,1);
- Улицей с наибольшим количеством заведений является Проспект Мира (183, из них 53 - кафе);
- На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73);
- Cамый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается;
- Большинство заведений работает ежедневно с 10 до 22, также часто встречается режим работы 24/7. Также режим работы зависит от типа заведения. Например столовые работают в основном с 9 до 18 (или 17);
- В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей. Средняя цена чашки кофе не превышает 100 рублей. Количество посадочных мест не превышает 400;
- Абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).
Рекомендации по открытию кофейни:
- Район с наибольшим спросом - ЦАО (428 кофейни) или с наименьшей конкуренцией - СЗАО (62);
- Режим работы - ежедневно с 10 до 22;
- Стоимость чашки капучино - до 200 рублей;
- Количество посадочных мест зависит от планируемой проходимости.